﻿/*
  KeePass Password Safe - The Open-Source Password Manager
  Copyright (C) 2003-2021 Dominik Reichl <dominik.reichl@t-online.de>

  This program is free software; you can redistribute it and/or modify
  it under the terms of the GNU General Public License as published by
  the Free Software Foundation; either version 2 of the License, or
  (at your option) any later version.

  This program is distributed in the hope that it will be useful,
  but WITHOUT ANY WARRANTY; without even the implied warranty of
  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  GNU General Public License for more details.

  You should have received a copy of the GNU General Public License
  along with this program; if not, write to the Free Software
  Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
*/

using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Text;

#if !KeePassUAP
using System.Drawing;
using System.Security.Cryptography;
#endif

using KeePassLib.Collections;
using KeePassLib.Cryptography;
using KeePassLib.Cryptography.PasswordGenerator;
using KeePassLib.Native;
using KeePassLib.Security;

namespace KeePassLib.Utility
{
  /// <summary>
  /// Character stream class.
  /// </summary>
  public sealed class CharStream
  {
    private readonly string m_str;
    private int m_iPos = 0;

    public long Position
    {
      get { return m_iPos; }
      set
      {
        if ((value < 0) || (value > int.MaxValue))
          throw new ArgumentOutOfRangeException("value");
        m_iPos = (int)value;
      }
    }

    public CharStream(string str)
    {
      if (str == null) { Debug.Assert(false); throw new ArgumentNullException("str"); }

      m_str = str;
    }

    public char ReadChar()
    {
      if (m_iPos >= m_str.Length) return char.MinValue;

      return m_str[m_iPos++];
    }

    public char ReadChar(bool bSkipWhiteSpace)
    {
      if (!bSkipWhiteSpace) return ReadChar();

      while (true)
      {
        char ch = ReadChar();
        if ((ch != ' ') && (ch != '\t') && (ch != '\r') && (ch != '\n'))
          return ch;
      }
    }

    public char PeekChar()
    {
      if (m_iPos >= m_str.Length) return char.MinValue;

      return m_str[m_iPos];
    }

    public char PeekChar(bool bSkipWhiteSpace)
    {
      if (!bSkipWhiteSpace) return PeekChar();

      int i = m_iPos;
      while (i < m_str.Length)
      {
        char ch = m_str[i];
        if ((ch != ' ') && (ch != '\t') && (ch != '\r') && (ch != '\n'))
          return ch;
        ++i;
      }

      return char.MinValue;
    }
  }

  public enum StrEncodingType
  {
    Unknown = 0,
    Default,
    Ascii,
    Utf7,
    Utf8,
    Utf16LE,
    Utf16BE,
    Utf32LE,
    Utf32BE
  }

  public sealed class StrEncodingInfo
  {
    private readonly StrEncodingType m_type;
    public StrEncodingType Type
    {
      get { return m_type; }
    }

    private readonly string m_strName;
    public string Name
    {
      get { return m_strName; }
    }

    private readonly Encoding m_enc;
    public Encoding Encoding
    {
      get { return m_enc; }
    }

    private readonly uint m_cbCodePoint;
    /// <summary>
    /// Size of a character in bytes.
    /// </summary>
    public uint CodePointSize
    {
      get { return m_cbCodePoint; }
    }

    private readonly byte[] m_vSig;
    /// <summary>
    /// Start signature of the text (byte order mark).
    /// May be <c>null</c> or empty, if no signature is known.
    /// </summary>
    public byte[] StartSignature
    {
      get { return m_vSig; }
    }

    public StrEncodingInfo(StrEncodingType t, string strName, Encoding enc,
        uint cbCodePoint, byte[] vStartSig)
    {
      if (strName == null) throw new ArgumentNullException("strName");
      if (enc == null) throw new ArgumentNullException("enc");
      if (cbCodePoint <= 0) throw new ArgumentOutOfRangeException("cbCodePoint");

      m_type = t;
      m_strName = strName;
      m_enc = enc;
      m_cbCodePoint = cbCodePoint;
      m_vSig = vStartSig;
    }
  }

  /// <summary>
  /// A class containing various string helper methods.
  /// </summary>
  public static class StrUtil
  {
    public static readonly StringComparison CaseIgnoreCmp = StringComparison.OrdinalIgnoreCase;

    public static StringComparer CaseIgnoreComparer
    {
      get { return StringComparer.OrdinalIgnoreCase; }
    }

    private static bool m_bRtl = false;
    public static bool RightToLeft
    {
      get { return m_bRtl; }
      set { m_bRtl = value; }
    }

    private static UTF8Encoding m_encUtf8 = null;
    public static UTF8Encoding Utf8
    {
      get
      {
        if (m_encUtf8 == null) m_encUtf8 = new UTF8Encoding(false, false);
        return m_encUtf8;
      }
    }

    private static List<StrEncodingInfo> m_lEncs = null;
    public static IEnumerable<StrEncodingInfo> Encodings
    {
      get
      {
        if (m_lEncs != null) return m_lEncs;

        List<StrEncodingInfo> l = new List<StrEncodingInfo>();

        l.Add(new StrEncodingInfo(StrEncodingType.Default,
#if KeePassUAP
					"Unicode (UTF-8)", StrUtil.Utf8, 1, new byte[] { 0xEF, 0xBB, 0xBF }));
#else
#if !KeePassLibSD
            Encoding.Default.EncodingName,
#else
					Encoding.Default.WebName,
#endif
            Encoding.Default,
            (uint)Encoding.Default.GetBytes("a").Length, null));
#endif

        l.Add(new StrEncodingInfo(StrEncodingType.Ascii,
            "ASCII", Encoding.ASCII, 1, null));
        l.Add(new StrEncodingInfo(StrEncodingType.Utf7,
            "Unicode (UTF-7)", Encoding.UTF7, 1, null));
        l.Add(new StrEncodingInfo(StrEncodingType.Utf8,
            "Unicode (UTF-8)", StrUtil.Utf8, 1, new byte[] { 0xEF, 0xBB, 0xBF }));
        l.Add(new StrEncodingInfo(StrEncodingType.Utf16LE,
            "Unicode (UTF-16 LE)", new UnicodeEncoding(false, false),
            2, new byte[] { 0xFF, 0xFE }));
        l.Add(new StrEncodingInfo(StrEncodingType.Utf16BE,
            "Unicode (UTF-16 BE)", new UnicodeEncoding(true, false),
            2, new byte[] { 0xFE, 0xFF }));

#if !KeePassLibSD
        l.Add(new StrEncodingInfo(StrEncodingType.Utf32LE,
            "Unicode (UTF-32 LE)", new UTF32Encoding(false, false),
            4, new byte[] { 0xFF, 0xFE, 0x0, 0x0 }));
        l.Add(new StrEncodingInfo(StrEncodingType.Utf32BE,
            "Unicode (UTF-32 BE)", new UTF32Encoding(true, false),
            4, new byte[] { 0x0, 0x0, 0xFE, 0xFF }));
#endif

        m_lEncs = l;
        return l;
      }
    }

    // public static string RtfPar
    // {
    //	// get { return (m_bRtl ? "\\rtlpar " : "\\par "); }
    //	get { return "\\par "; }
    // }

    // /// <summary>
    // /// Convert a string into a valid RTF string.
    // /// </summary>
    // /// <param name="str">Any string.</param>
    // /// <returns>RTF-encoded string.</returns>
    // public static string MakeRtfString(string str)
    // {
    //	Debug.Assert(str != null); if(str == null) throw new ArgumentNullException("str");
    //	str = str.Replace("\\", "\\\\");
    //	str = str.Replace("\r", string.Empty);
    //	str = str.Replace("{", "\\{");
    //	str = str.Replace("}", "\\}");
    //	str = str.Replace("\n", StrUtil.RtfPar);
    //	StringBuilder sbEncoded = new StringBuilder();
    //	for(int i = 0; i < str.Length; ++i)
    //	{
    //		char ch = str[i];
    //		if((int)ch >= 256)
    //			sbEncoded.Append(StrUtil.RtfEncodeChar(ch));
    //		else sbEncoded.Append(ch);
    //	}
    //	return sbEncoded.ToString();
    // }

    public static string RtfEncodeChar(char ch)
    {
      // Unicode character values must be encoded using
      // 16-bit numbers (decimal); Unicode values greater
      // than 32767 must be expressed as negative numbers
      short sh = (short)ch;
      return ("\\u" + sh.ToString(NumberFormatInfo.InvariantInfo) + "?");
    }

    internal static bool RtfIsURtf(string str)
    {
      if (str == null) { Debug.Assert(false); return false; }

      const string p = "{\\urtf"; // Typically "{\\urtf1\\ansi\\ansicpg65001"
      return (str.StartsWith(p) && (str.Length > p.Length) &&
          char.IsDigit(str[p.Length]));
    }

    public static string RtfFix(string strRtf)
    {
      if (strRtf == null) { Debug.Assert(false); return string.Empty; }

      string str = strRtf;

      // Workaround for .NET bug: the Rtf property of a RichTextBox
      // can return an RTF text starting with "{\\urtf", but
      // setting such an RTF text throws an exception (the setter
      // checks for the RTF text to start with "{\\rtf");
      // https://sourceforge.net/p/keepass/discussion/329221/thread/7788872f/
      // https://www.microsoft.com/en-us/download/details.aspx?id=10725
      // https://msdn.microsoft.com/en-us/library/windows/desktop/bb774284.aspx
      // https://referencesource.microsoft.com/#System.Windows.Forms/winforms/Managed/System/WinForms/RichTextBox.cs
      if (RtfIsURtf(str)) str = str.Remove(2, 1); // Remove the 'u'

      return str;
    }

    internal static string RtfFilterText(string strText)
    {
      if (strText == null) { Debug.Assert(false); return string.Empty; }

      // A U+FFFC character causes the rest of the text to be lost.
      // With '?', the string length, substring indices and
      // character visibility remain the same.
      // More special characters (unproblematic) in rich text boxes:
      // https://docs.microsoft.com/en-us/windows/win32/api/richedit/ns-richedit-gettextex
      return strText.Replace('\uFFFC', '?');
    }

    internal static bool ContainsHighChar(string str)
    {
      if (str == null) { Debug.Assert(false); return false; }

      for (int i = 0; i < str.Length; ++i)
      {
        if (str[i] > '\u00FF') return true;
      }

      return false;
    }

    /// <summary>
    /// Convert a string to a HTML sequence representing that string.
    /// </summary>
    /// <param name="str">String to convert.</param>
    /// <returns>String, HTML-encoded.</returns>
    public static string StringToHtml(string str)
    {
      return StringToHtml(str, false);
    }

    internal static string StringToHtml(string str, bool bNbsp)
    {
      Debug.Assert(str != null); if (str == null) throw new ArgumentNullException("str");

      str = str.Replace(@"&", @"&amp;"); // Must be first
      str = str.Replace(@"<", @"&lt;");
      str = str.Replace(@">", @"&gt;");
      str = str.Replace("\"", @"&quot;");
      str = str.Replace("\'", @"&#39;");

      if (bNbsp) str = str.Replace(" ", @"&nbsp;"); // Before <br />

      str = NormalizeNewLines(str, false);
      str = str.Replace("\n", @"<br />" + MessageService.NewLine);

      return str;
    }

    public static string XmlToString(string str)
    {
      Debug.Assert(str != null); if (str == null) throw new ArgumentNullException("str");

      str = str.Replace(@"&amp;", @"&");
      str = str.Replace(@"&lt;", @"<");
      str = str.Replace(@"&gt;", @">");
      str = str.Replace(@"&quot;", "\"");
      str = str.Replace(@"&#39;", "\'");

      return str;
    }

    public static string ReplaceCaseInsensitive(string strString, string strFind,
        string strNew)
    {
      Debug.Assert(strString != null); if (strString == null) return strString;
      Debug.Assert(strFind != null); if (strFind == null) return strString;
      Debug.Assert(strNew != null); if (strNew == null) return strString;

      string str = strString;

      int nPos = 0;
      while (nPos < str.Length)
      {
        nPos = str.IndexOf(strFind, nPos, StringComparison.OrdinalIgnoreCase);
        if (nPos < 0) break;

        str = str.Remove(nPos, strFind.Length);
        str = str.Insert(nPos, strNew);

        nPos += strNew.Length;
      }

      return str;
    }

    // /// <summary>
    // /// Initialize an RTF document based on given font face and size.
    // /// </summary>
    // /// <param name="sb"><c>StringBuilder</c> to put the generated RTF into.</param>
    // /// <param name="strFontFace">Face name of the font to use.</param>
    // /// <param name="fFontSize">Size of the font to use.</param>
    // public static void InitRtf(StringBuilder sb, string strFontFace, float fFontSize)
    // {
    //	Debug.Assert(sb != null); if(sb == null) throw new ArgumentNullException("sb");
    //	Debug.Assert(strFontFace != null); if(strFontFace == null) throw new ArgumentNullException("strFontFace");
    //	sb.Append("{\\rtf1");
    //	if(m_bRtl) sb.Append("\\fbidis");
    //	sb.Append("\\ansi\\ansicpg");
    //	sb.Append(Encoding.Default.CodePage);
    //	sb.Append("\\deff0{\\fonttbl{\\f0\\fswiss MS Sans Serif;}{\\f1\\froman\\fcharset2 Symbol;}{\\f2\\fswiss ");
    //	sb.Append(strFontFace);
    //	sb.Append(";}{\\f3\\fswiss Arial;}}");
    //	sb.Append("{\\colortbl\\red0\\green0\\blue0;}");
    //	if(m_bRtl) sb.Append("\\rtldoc");
    //	sb.Append("\\deflang1031\\pard\\plain\\f2\\cf0 ");
    //	sb.Append("\\fs");
    //	sb.Append((int)(fFontSize * 2));
    //	if(m_bRtl) sb.Append("\\rtlpar\\qr\\rtlch ");
    // }

    // /// <summary>
    // /// Convert a simple HTML string to an RTF string.
    // /// </summary>
    // /// <param name="strHtmlString">Input HTML string.</param>
    // /// <returns>RTF string representing the HTML input string.</returns>
    // public static string SimpleHtmlToRtf(string strHtmlString)
    // {
    //	StringBuilder sb = new StringBuilder();
    //	StrUtil.InitRtf(sb, "Microsoft Sans Serif", 8.25f);
    //	sb.Append(" ");
    //	string str = MakeRtfString(strHtmlString);
    //	str = str.Replace("<b>", "\\b ");
    //	str = str.Replace("</b>", "\\b0 ");
    //	str = str.Replace("<i>", "\\i ");
    //	str = str.Replace("</i>", "\\i0 ");
    //	str = str.Replace("<u>", "\\ul ");
    //	str = str.Replace("</u>", "\\ul0 ");
    //	str = str.Replace("<br />", StrUtil.RtfPar);
    //	sb.Append(str);
    //	return sb.ToString();
    // }

    /// <summary>
    /// Convert a <c>Color</c> to a HTML color identifier string.
    /// </summary>
    /// <param name="color">Color to convert.</param>
    /// <param name="bEmptyIfTransparent">If this is <c>true</c>, an empty string
    /// is returned if the color is transparent.</param>
    /// <returns>HTML color identifier string.</returns>
    public static string ColorToUnnamedHtml(Color color, bool bEmptyIfTransparent)
    {
      if (bEmptyIfTransparent && (color.A != 255))
        return string.Empty;

      StringBuilder sb = new StringBuilder();
      byte bt;

      sb.Append('#');

      bt = (byte)(color.R >> 4);
      if (bt < 10) sb.Append((char)('0' + bt)); else sb.Append((char)('A' - 10 + bt));
      bt = (byte)(color.R & 0x0F);
      if (bt < 10) sb.Append((char)('0' + bt)); else sb.Append((char)('A' - 10 + bt));

      bt = (byte)(color.G >> 4);
      if (bt < 10) sb.Append((char)('0' + bt)); else sb.Append((char)('A' - 10 + bt));
      bt = (byte)(color.G & 0x0F);
      if (bt < 10) sb.Append((char)('0' + bt)); else sb.Append((char)('A' - 10 + bt));

      bt = (byte)(color.B >> 4);
      if (bt < 10) sb.Append((char)('0' + bt)); else sb.Append((char)('A' - 10 + bt));
      bt = (byte)(color.B & 0x0F);
      if (bt < 10) sb.Append((char)('0' + bt)); else sb.Append((char)('A' - 10 + bt));

      return sb.ToString();
    }

    /// <summary>
    /// Format an exception and convert it to a string.
    /// </summary>
    /// <param name="excp"><c>Exception</c> to convert/format.</param>
    /// <returns>String representing the exception.</returns>
    public static string FormatException(Exception excp)
    {
      string strText = string.Empty;

      if (!string.IsNullOrEmpty(excp.Message))
        strText += excp.Message + MessageService.NewLine;
#if !KeePassLibSD
      if (!string.IsNullOrEmpty(excp.Source))
        strText += excp.Source + MessageService.NewLine;
#endif
      if (!string.IsNullOrEmpty(excp.StackTrace))
        strText += excp.StackTrace + MessageService.NewLine;
#if !KeePassLibSD
#if !KeePassUAP
      if (excp.TargetSite != null)
        strText += excp.TargetSite.ToString() + MessageService.NewLine;
#endif

      if (excp.Data != null)
      {
        strText += MessageService.NewLine;
        foreach (DictionaryEntry de in excp.Data)
          strText += @"'" + de.Key + @"' -> '" + de.Value + @"'" +
              MessageService.NewLine;
      }
#endif

      if (excp.InnerException != null)
      {
        strText += MessageService.NewLine + "Inner:" + MessageService.NewLine;
        if (!string.IsNullOrEmpty(excp.InnerException.Message))
          strText += excp.InnerException.Message + MessageService.NewLine;
#if !KeePassLibSD
        if (!string.IsNullOrEmpty(excp.InnerException.Source))
          strText += excp.InnerException.Source + MessageService.NewLine;
#endif
        if (!string.IsNullOrEmpty(excp.InnerException.StackTrace))
          strText += excp.InnerException.StackTrace + MessageService.NewLine;
#if !KeePassLibSD
#if !KeePassUAP
        if (excp.InnerException.TargetSite != null)
          strText += excp.InnerException.TargetSite.ToString();
#endif

        if (excp.InnerException.Data != null)
        {
          strText += MessageService.NewLine;
          foreach (DictionaryEntry de in excp.InnerException.Data)
            strText += @"'" + de.Key + @"' -> '" + de.Value + @"'" +
                MessageService.NewLine;
        }
#endif
      }

      return strText;
    }

    public static bool TryParseUShort(string str, out ushort u)
    {
#if !KeePassLibSD
      return ushort.TryParse(str, out u);
#else
			try { u = ushort.Parse(str); return true; }
			catch(Exception) { u = 0; return false; }
#endif
    }

    public static bool TryParseInt(string str, out int n)
    {
#if !KeePassLibSD
      return int.TryParse(str, out n);
#else
			try { n = int.Parse(str); return true; }
			catch(Exception) { n = 0; }
			return false;
#endif
    }

    public static bool TryParseIntInvariant(string str, out int n)
    {
#if !KeePassLibSD
      return int.TryParse(str, NumberStyles.Integer,
          NumberFormatInfo.InvariantInfo, out n);
#else
			try
			{
				n = int.Parse(str, NumberStyles.Integer,
					NumberFormatInfo.InvariantInfo);
				return true;
			}
			catch(Exception) { n = 0; }
			return false;
#endif
    }

    public static bool TryParseUInt(string str, out uint u)
    {
#if !KeePassLibSD
      return uint.TryParse(str, out u);
#else
			try { u = uint.Parse(str); return true; }
			catch(Exception) { u = 0; }
			return false;
#endif
    }

    public static bool TryParseUIntInvariant(string str, out uint u)
    {
#if !KeePassLibSD
      return uint.TryParse(str, NumberStyles.Integer,
          NumberFormatInfo.InvariantInfo, out u);
#else
			try
			{
				u = uint.Parse(str, NumberStyles.Integer,
					NumberFormatInfo.InvariantInfo);
				return true;
			}
			catch(Exception) { u = 0; }
			return false;
#endif
    }

    public static bool TryParseLong(string str, out long n)
    {
#if !KeePassLibSD
      return long.TryParse(str, out n);
#else
			try { n = long.Parse(str); return true; }
			catch(Exception) { n = 0; }
			return false;
#endif
    }

    public static bool TryParseLongInvariant(string str, out long n)
    {
#if !KeePassLibSD
      return long.TryParse(str, NumberStyles.Integer,
          NumberFormatInfo.InvariantInfo, out n);
#else
			try
			{
				n = long.Parse(str, NumberStyles.Integer,
					NumberFormatInfo.InvariantInfo);
				return true;
			}
			catch(Exception) { n = 0; }
			return false;
#endif
    }

    public static bool TryParseULong(string str, out ulong u)
    {
#if !KeePassLibSD
      return ulong.TryParse(str, out u);
#else
			try { u = ulong.Parse(str); return true; }
			catch(Exception) { u = 0; }
			return false;
#endif
    }

    public static bool TryParseULongInvariant(string str, out ulong u)
    {
#if !KeePassLibSD
      return ulong.TryParse(str, NumberStyles.Integer,
          NumberFormatInfo.InvariantInfo, out u);
#else
			try
			{
				u = ulong.Parse(str, NumberStyles.Integer,
					NumberFormatInfo.InvariantInfo);
				return true;
			}
			catch(Exception) { u = 0; }
			return false;
#endif
    }

    public static bool TryParseDateTime(string str, out DateTime dt)
    {
#if !KeePassLibSD
      return DateTime.TryParse(str, out dt);
#else
			try { dt = DateTime.Parse(str); return true; }
			catch(Exception) { dt = DateTime.UtcNow; }
			return false;
#endif
    }

    public static string CompactString3Dots(string strText, int cchMax)
    {
      Debug.Assert(strText != null);
      if (strText == null) throw new ArgumentNullException("strText");
      Debug.Assert(cchMax >= 0);
      if (cchMax < 0) throw new ArgumentOutOfRangeException("cchMax");

      if (strText.Length <= cchMax) return strText;

      if (cchMax == 0) return string.Empty;
      if (cchMax <= 3) return new string('.', cchMax);

      return (strText.Substring(0, cchMax - 3) + "...");
    }

    private static readonly char[] g_vDots = new char[] { '.', '\u2026' };
    private static readonly char[] g_vDotsWS = new char[] { '.', '\u2026',
            ' ', '\t', '\r', '\n' };
    internal static string TrimDots(string strText, bool bTrimWhiteSpace)
    {
      if (strText == null) { Debug.Assert(false); return string.Empty; }

      return strText.Trim(bTrimWhiteSpace ? g_vDotsWS : g_vDots);
    }

    public static string GetStringBetween(string strText, int nStartIndex,
        string strStart, string strEnd)
    {
      int nTemp;
      return GetStringBetween(strText, nStartIndex, strStart, strEnd, out nTemp);
    }

    public static string GetStringBetween(string strText, int nStartIndex,
        string strStart, string strEnd, out int nInnerStartIndex)
    {
      if (strText == null) throw new ArgumentNullException("strText");
      if (strStart == null) throw new ArgumentNullException("strStart");
      if (strEnd == null) throw new ArgumentNullException("strEnd");

      nInnerStartIndex = -1;

      int nIndex = strText.IndexOf(strStart, nStartIndex);
      if (nIndex < 0) return string.Empty;

      nIndex += strStart.Length;

      int nEndIndex = strText.IndexOf(strEnd, nIndex);
      if (nEndIndex < 0) return string.Empty;

      nInnerStartIndex = nIndex;
      return strText.Substring(nIndex, nEndIndex - nIndex);
    }

    /// <summary>
    /// Removes all characters that are not valid XML characters,
    /// according to https://www.w3.org/TR/xml/#charsets .
    /// </summary>
    /// <param name="strText">Source text.</param>
    /// <returns>Text containing only valid XML characters.</returns>
    public static string SafeXmlString(string strText)
    {
      Debug.Assert(strText != null); // No throw
      if (string.IsNullOrEmpty(strText)) return strText;

      int nLength = strText.Length;
      StringBuilder sb = new StringBuilder(nLength);

      for (int i = 0; i < nLength; ++i)
      {
        char ch = strText[i];

        if (((ch >= '\u0020') && (ch <= '\uD7FF')) ||
            (ch == '\u0009') || (ch == '\u000A') || (ch == '\u000D') ||
            ((ch >= '\uE000') && (ch <= '\uFFFD')))
          sb.Append(ch);
        else if ((ch >= '\uD800') && (ch <= '\uDBFF')) // High surrogate
        {
          if ((i + 1) < nLength)
          {
            char chLow = strText[i + 1];
            if ((chLow >= '\uDC00') && (chLow <= '\uDFFF')) // Low sur.
            {
              sb.Append(ch);
              sb.Append(chLow);
              ++i;
            }
            else { Debug.Assert(false); } // Low sur. invalid
          }
          else { Debug.Assert(false); } // Low sur. missing
        }

        Debug.Assert((ch < '\uDC00') || (ch > '\uDFFF')); // Lonely low sur.
      }

      return sb.ToString();
    }

    /* private static Regex g_rxNaturalSplit = null;
    public static int CompareNaturally(string strX, string strY)
    {
        Debug.Assert(strX != null);
        if(strX == null) throw new ArgumentNullException("strX");
        Debug.Assert(strY != null);
        if(strY == null) throw new ArgumentNullException("strY");

        if(NativeMethods.SupportsStrCmpNaturally)
            return NativeMethods.StrCmpNaturally(strX, strY);

        if(g_rxNaturalSplit == null)
            g_rxNaturalSplit = new Regex(@"([0-9]+)", RegexOptions.Compiled);

        string[] vPartsX = g_rxNaturalSplit.Split(strX);
        string[] vPartsY = g_rxNaturalSplit.Split(strY);

        int n = Math.Min(vPartsX.Length, vPartsY.Length);
        for(int i = 0; i < n; ++i)
        {
            string strPartX = vPartsX[i], strPartY = vPartsY[i];
            int iPartCompare;

#if KeePassLibSD
            try
            {
                ulong uX = ulong.Parse(strPartX);
                ulong uY = ulong.Parse(strPartY);
                iPartCompare = uX.CompareTo(uY);
            }
            catch(Exception) { iPartCompare = string.Compare(strPartX, strPartY, true); }
#else
            ulong uX, uY;
            if(ulong.TryParse(strPartX, out uX) && ulong.TryParse(strPartY, out uY))
                iPartCompare = uX.CompareTo(uY);
            else iPartCompare = string.Compare(strPartX, strPartY, true);
#endif

            if(iPartCompare != 0) return iPartCompare;
        }

        if(vPartsX.Length == vPartsY.Length) return 0;
        if(vPartsX.Length < vPartsY.Length) return -1;
        return 1;
    } */

    public static int CompareNaturally(string strX, string strY)
    {
      Debug.Assert(strX != null);
      if (strX == null) throw new ArgumentNullException("strX");
      Debug.Assert(strY != null);
      if (strY == null) throw new ArgumentNullException("strY");

      if (NativeMethods.SupportsStrCmpNaturally)
        return NativeMethods.StrCmpNaturally(strX, strY);

      int cX = strX.Length;
      int cY = strY.Length;
      if (cX == 0) return ((cY == 0) ? 0 : -1);
      if (cY == 0) return 1;

      char chFirstX = strX[0];
      char chFirstY = strY[0];
      bool bExpNum = ((chFirstX >= '0') && (chFirstX <= '9'));
      bool bExpNumY = ((chFirstY >= '0') && (chFirstY <= '9'));
      if (bExpNum != bExpNumY) return string.Compare(strX, strY, true);

      int pX = 0;
      int pY = 0;
      while ((pX < cX) && (pY < cY))
      {
        Debug.Assert(((strX[pX] >= '0') && (strX[pX] <= '9')) == bExpNum);
        Debug.Assert(((strY[pY] >= '0') && (strY[pY] <= '9')) == bExpNum);

        int pExclX = pX + 1;
        while (pExclX < cX)
        {
          char ch = strX[pExclX];
          bool bChNum = ((ch >= '0') && (ch <= '9'));
          if (bChNum != bExpNum) break;
          ++pExclX;
        }

        int pExclY = pY + 1;
        while (pExclY < cY)
        {
          char ch = strY[pExclY];
          bool bChNum = ((ch >= '0') && (ch <= '9'));
          if (bChNum != bExpNum) break;
          ++pExclY;
        }

        string strPartX = strX.Substring(pX, pExclX - pX);
        string strPartY = strY.Substring(pY, pExclY - pY);

        bool bStrCmp = true;
        if (bExpNum)
        {
          // 2^64 - 1 = 18446744073709551615 has length 20
          if ((strPartX.Length <= 19) && (strPartY.Length <= 19))
          {
            ulong uX, uY;
            if (ulong.TryParse(strPartX, out uX) && ulong.TryParse(strPartY, out uY))
            {
              if (uX < uY) return -1;
              if (uX > uY) return 1;

              bStrCmp = false;
            }
            else { Debug.Assert(false); }
          }
          else
          {
            double dX, dY;
            if (double.TryParse(strPartX, out dX) && double.TryParse(strPartY, out dY))
            {
              if (dX < dY) return -1;
              if (dX > dY) return 1;

              bStrCmp = false;
            }
            else { Debug.Assert(false); }
          }
        }
        if (bStrCmp)
        {
          int c = string.Compare(strPartX, strPartY, true);
          if (c != 0) return c;
        }

        bExpNum = !bExpNum;
        pX = pExclX;
        pY = pExclY;
      }

      if (pX >= cX)
      {
        Debug.Assert(pX == cX);
        if (pY >= cY) { Debug.Assert(pY == cY); return 0; }
        return -1;
      }

      Debug.Assert(pY == cY);
      return 1;
    }

    public static string RemoveAccelerator(string strMenuText)
    {
      if (strMenuText == null) throw new ArgumentNullException("strMenuText");

      string str = strMenuText;

      for (char ch = 'A'; ch <= 'Z'; ++ch)
      {
        string strEnhAcc = @"(&" + ch.ToString() + ")";
        if (str.IndexOf(strEnhAcc) >= 0)
        {
          str = str.Replace(" " + strEnhAcc, string.Empty);
          str = str.Replace(strEnhAcc, string.Empty);
        }
      }

      str = str.Replace(@"&", string.Empty);

      return str;
    }

    public static string AddAccelerator(string strMenuText,
        List<char> lAvailKeys)
    {
      if (strMenuText == null) { Debug.Assert(false); return string.Empty; }
      if (lAvailKeys == null) { Debug.Assert(false); return strMenuText; }

      for (int i = 0; i < strMenuText.Length; ++i)
      {
        char ch = char.ToLowerInvariant(strMenuText[i]);

        for (int j = 0; j < lAvailKeys.Count; ++j)
        {
          if (char.ToLowerInvariant(lAvailKeys[j]) == ch)
          {
            lAvailKeys.RemoveAt(j);
            return strMenuText.Insert(i, @"&");
          }
        }
      }

      return strMenuText;
    }

    public static string EncodeMenuText(string strText)
    {
      if (strText == null) throw new ArgumentNullException("strText");

      return strText.Replace(@"&", @"&&");
    }

    public static string EncodeToolTipText(string strText)
    {
      if (strText == null) throw new ArgumentNullException("strText");

      return strText.Replace(@"&", @"&&&");
    }

    public static bool IsHexString(string str, bool bStrict)
    {
      if (str == null) throw new ArgumentNullException("str");

      foreach (char ch in str)
      {
        if ((ch >= '0') && (ch <= '9')) continue;
        if ((ch >= 'a') && (ch <= 'f')) continue;
        if ((ch >= 'A') && (ch <= 'F')) continue;

        if (bStrict) return false;

        if ((ch == ' ') || (ch == '\t') || (ch == '\r') || (ch == '\n'))
          continue;

        return false;
      }

      return true;
    }

    public static bool IsHexString(byte[] pbUtf8, bool bStrict)
    {
      if (pbUtf8 == null) throw new ArgumentNullException("pbUtf8");

      for (int i = 0; i < pbUtf8.Length; ++i)
      {
        byte bt = pbUtf8[i];
        if ((bt >= (byte)'0') && (bt <= (byte)'9')) continue;
        if ((bt >= (byte)'a') && (bt <= (byte)'f')) continue;
        if ((bt >= (byte)'A') && (bt <= (byte)'F')) continue;

        if (bStrict) return false;

        if ((bt == (byte)' ') || (bt == (byte)'\t') ||
            (bt == (byte)'\r') || (bt == (byte)'\n'))
          continue;

        return false;
      }

      return true;
    }

#if !KeePassLibSD
    private static readonly char[] g_vPatternPartsSep = new char[] { '*' };
    public static bool SimplePatternMatch(string strPattern, string strText,
        StringComparison sc)
    {
      if (strPattern == null) throw new ArgumentNullException("strPattern");
      if (strText == null) throw new ArgumentNullException("strText");

      if (strPattern.IndexOf('*') < 0) return strText.Equals(strPattern, sc);

      string[] vPatternParts = strPattern.Split(g_vPatternPartsSep,
          StringSplitOptions.RemoveEmptyEntries);
      if (vPatternParts == null) { Debug.Assert(false); return true; }
      if (vPatternParts.Length == 0) return true;

      if (strText.Length == 0) return false;

      if ((strPattern[0] != '*') && !strText.StartsWith(vPatternParts[0], sc))
        return false;
      if ((strPattern[strPattern.Length - 1] != '*') && !strText.EndsWith(
          vPatternParts[vPatternParts.Length - 1], sc))
        return false;

      int iOffset = 0;
      for (int i = 0; i < vPatternParts.Length; ++i)
      {
        string strPart = vPatternParts[i];

        int iFound = strText.IndexOf(strPart, iOffset, sc);
        if (iFound < iOffset) return false;

        iOffset = iFound + strPart.Length;
        if (iOffset == strText.Length) return (i == (vPatternParts.Length - 1));
      }

      return true;
    }
#endif // !KeePassLibSD

    public static bool StringToBool(string str)
    {
      if (string.IsNullOrEmpty(str)) return false; // No assert

      string s = str.Trim().ToLower();
      if (s == "true") return true;
      if (s == "yes") return true;
      if (s == "1") return true;
      if (s == "enabled") return true;
      if (s == "checked") return true;

      return false;
    }

    public static bool? StringToBoolEx(string str)
    {
      if (string.IsNullOrEmpty(str)) return null;

      string s = str.Trim().ToLower();
      if (s == "true") return true;
      if (s == "false") return false;

      return null;
    }

    public static string BoolToString(bool bValue)
    {
      return (bValue ? "true" : "false");
    }

    public static string BoolToStringEx(bool? bValue)
    {
      if (bValue.HasValue) return BoolToString(bValue.Value);
      return "null";
    }

    /// <summary>
    /// Normalize new line characters in a string. Input strings may
    /// contain mixed new line character sequences from all commonly
    /// used operating systems (i.e. \r\n from Windows, \n from Unix
    /// and \r from Mac OS.
    /// </summary>
    /// <param name="str">String with mixed new line characters.</param>
    /// <param name="bWindows">If <c>true</c>, new line characters
    /// are normalized for Windows (\r\n); if <c>false</c>, new line
    /// characters are normalized for Unix (\n).</param>
    /// <returns>String with normalized new line characters.</returns>
    public static string NormalizeNewLines(string str, bool bWindows)
    {
      if (string.IsNullOrEmpty(str)) return str;

      str = str.Replace("\r\n", "\n");
      str = str.Replace("\r", "\n");

      if (bWindows) str = str.Replace("\n", "\r\n");

      return str;
    }

    public static void NormalizeNewLines(ProtectedStringDictionary dict,
        bool bWindows)
    {
      if (dict == null) { Debug.Assert(false); return; }

      List<string> lKeys = dict.GetKeys();
      foreach (string strKey in lKeys)
      {
        ProtectedString ps = dict.Get(strKey);
        if (ps == null) { Debug.Assert(false); continue; }

        char[] v = ps.ReadChars();
        if (!IsNewLineNormalized(v, bWindows))
          dict.Set(strKey, new ProtectedString(ps.IsProtected,
              NormalizeNewLines(ps.ReadString(), bWindows)));
        MemUtil.ZeroArray<char>(v);
      }
    }

    internal static bool IsNewLineNormalized(char[] v, bool bWindows)
    {
      if (v == null) { Debug.Assert(false); return true; }

      if (bWindows)
      {
        int iFreeCr = -2; // Must be < -1 (for test "!= (i - 1)")

        for (int i = 0; i < v.Length; ++i)
        {
          char ch = v[i];

          if (ch == '\r')
          {
            if (iFreeCr >= 0) return false;
            iFreeCr = i;
          }
          else if (ch == '\n')
          {
            if (iFreeCr != (i - 1)) return false;
            iFreeCr = -2; // Consume \r
          }
        }

        return (iFreeCr < 0); // Ensure no \r at end
      }

      return (Array.IndexOf<char>(v, '\r') < 0);
    }

    public static string GetNewLineSeq(string str)
    {
      if (str == null) { Debug.Assert(false); return MessageService.NewLine; }

      int n = str.Length, nLf = 0, nCr = 0, nCrLf = 0;
      char chLast = char.MinValue;
      for (int i = 0; i < n; ++i)
      {
        char ch = str[i];

        if (ch == '\r') ++nCr;
        else if (ch == '\n')
        {
          ++nLf;
          if (chLast == '\r') ++nCrLf;
        }

        chLast = ch;
      }

      nCr -= nCrLf;
      nLf -= nCrLf;

      int nMax = Math.Max(nCrLf, Math.Max(nCr, nLf));
      if (nMax == 0) return MessageService.NewLine;

      if (nCrLf == nMax) return "\r\n";
      return ((nLf == nMax) ? "\n" : "\r");
    }

    public static string AlphaNumericOnly(string str)
    {
      if (string.IsNullOrEmpty(str)) return str;

      StringBuilder sb = new StringBuilder();
      for (int i = 0; i < str.Length; ++i)
      {
        char ch = str[i];
        if (((ch >= 'a') && (ch <= 'z')) || ((ch >= 'A') && (ch <= 'Z')) ||
            ((ch >= '0') && (ch <= '9')))
          sb.Append(ch);
      }

      return sb.ToString();
    }

    public static string FormatDataSize(ulong uBytes)
    {
      const ulong uKB = 1024;
      const ulong uMB = uKB * uKB;
      const ulong uGB = uMB * uKB;
      const ulong uTB = uGB * uKB;

      if (uBytes == 0) return "0 KB";
      if (uBytes <= uKB) return "1 KB";
      if (uBytes <= uMB) return (((uBytes - 1UL) / uKB) + 1UL).ToString() + " KB";
      if (uBytes <= uGB) return (((uBytes - 1UL) / uMB) + 1UL).ToString() + " MB";
      if (uBytes <= uTB) return (((uBytes - 1UL) / uGB) + 1UL).ToString() + " GB";

      return (((uBytes - 1UL) / uTB) + 1UL).ToString() + " TB";
    }

    public static string FormatDataSizeKB(ulong uBytes)
    {
      const ulong uKB = 1024;

      if (uBytes == 0) return "0 KB";
      if (uBytes <= uKB) return "1 KB";

      return (((uBytes - 1UL) / uKB) + 1UL).ToString() + " KB";
    }

    private static readonly char[] m_vVersionSep = new char[] { '.', ',' };
    public static ulong ParseVersion(string strVersion)
    {
      if (strVersion == null) { Debug.Assert(false); return 0; }

      string[] vVer = strVersion.Split(m_vVersionSep);
      if ((vVer == null) || (vVer.Length == 0)) { Debug.Assert(false); return 0; }

      ushort uPart;
      StrUtil.TryParseUShort(vVer[0].Trim(), out uPart);
      ulong uVer = ((ulong)uPart << 48);

      if (vVer.Length >= 2)
      {
        StrUtil.TryParseUShort(vVer[1].Trim(), out uPart);
        uVer |= ((ulong)uPart << 32);
      }

      if (vVer.Length >= 3)
      {
        StrUtil.TryParseUShort(vVer[2].Trim(), out uPart);
        uVer |= ((ulong)uPart << 16);
      }

      if (vVer.Length >= 4)
      {
        StrUtil.TryParseUShort(vVer[3].Trim(), out uPart);
        uVer |= (ulong)uPart;
      }

      return uVer;
    }

    public static string VersionToString(ulong uVersion)
    {
      return VersionToString(uVersion, 1U);
    }

    [Obsolete]
    public static string VersionToString(ulong uVersion,
        bool bEnsureAtLeastTwoComp)
    {
      return VersionToString(uVersion, (bEnsureAtLeastTwoComp ? 2U : 1U));
    }

    public static string VersionToString(ulong uVersion, uint uMinComp)
    {
      StringBuilder sb = new StringBuilder();
      uint uComp = 0;

      for (int i = 0; i < 4; ++i)
      {
        if (uVersion == 0UL) break;

        ushort us = (ushort)(uVersion >> 48);

        if (sb.Length > 0) sb.Append('.');

        sb.Append(us.ToString(NumberFormatInfo.InvariantInfo));
        ++uComp;

        uVersion <<= 16;
      }

      while (uComp < uMinComp)
      {
        if (sb.Length > 0) sb.Append('.');

        sb.Append('0');
        ++uComp;
      }

      return sb.ToString();
    }

    private static readonly byte[] m_pbOptEnt = { 0xA5, 0x74, 0x2E, 0xEC };

    public static string EncryptString(string strPlainText)
    {
      if (string.IsNullOrEmpty(strPlainText)) return string.Empty;

      try
      {
        byte[] pbPlain = StrUtil.Utf8.GetBytes(strPlainText);
        byte[] pbEnc = CryptoUtil.ProtectData(pbPlain, m_pbOptEnt,
            DataProtectionScope.CurrentUser);

#if (!KeePassLibSD && !KeePassUAP)
        return Convert.ToBase64String(pbEnc, Base64FormattingOptions.None);
#else
				return Convert.ToBase64String(pbEnc);
#endif
      }
      catch (Exception) { Debug.Assert(false); }

      return strPlainText;
    }

    public static string DecryptString(string strCipherText)
    {
      if (string.IsNullOrEmpty(strCipherText)) return string.Empty;

      try
      {
        byte[] pbEnc = Convert.FromBase64String(strCipherText);
        byte[] pbPlain = CryptoUtil.UnprotectData(pbEnc, m_pbOptEnt,
            DataProtectionScope.CurrentUser);

        return StrUtil.Utf8.GetString(pbPlain, 0, pbPlain.Length);
      }
      catch (Exception) { Debug.Assert(false); }

      return strCipherText;
    }

    public static string SerializeIntArray(int[] vNumbers)
    {
      if (vNumbers == null) throw new ArgumentNullException("vNumbers");

      StringBuilder sb = new StringBuilder();
      for (int i = 0; i < vNumbers.Length; ++i)
      {
        if (i > 0) sb.Append(' ');
        sb.Append(vNumbers[i].ToString(NumberFormatInfo.InvariantInfo));
      }

      return sb.ToString();
    }

    public static int[] DeserializeIntArray(string strSerialized)
    {
      if (strSerialized == null) throw new ArgumentNullException("strSerialized");
      if (strSerialized.Length == 0) return new int[0];

      string[] vParts = strSerialized.Split(' ');
      int[] v = new int[vParts.Length];

      for (int i = 0; i < vParts.Length; ++i)
      {
        int n;
        if (!TryParseIntInvariant(vParts[i], out n)) { Debug.Assert(false); }
        v[i] = n;
      }

      return v;
    }

    private static readonly char[] g_vTagSep = new char[] { ',', ';' };
    internal static string NormalizeTag(string strTag)
    {
      if (strTag == null) { Debug.Assert(false); return string.Empty; }

      strTag = strTag.Trim();

      for (int i = g_vTagSep.Length - 1; i >= 0; --i)
        strTag = strTag.Replace(g_vTagSep[i], '.');

      return strTag;
    }

    internal static void NormalizeTags(List<string> lTags)
    {
      if (lTags == null) { Debug.Assert(false); return; }

      bool bRemoveNulls = false;
      for (int i = lTags.Count - 1; i >= 0; --i)
      {
        string str = NormalizeTag(lTags[i]);

        if (string.IsNullOrEmpty(str))
        {
          lTags[i] = null;
          bRemoveNulls = true;
        }
        else lTags[i] = str;
      }

      if (bRemoveNulls)
      {
        Predicate<string> f = delegate (string str) { return (str == null); };
        lTags.RemoveAll(f);
      }

      if (lTags.Count >= 2)
      {
        // Deduplicate
        Dictionary<string, bool> d = new Dictionary<string, bool>();
        for (int i = lTags.Count - 1; i >= 0; --i)
          d[lTags[i]] = true;
        if (d.Count != lTags.Count)
        {
          lTags.Clear();
          lTags.AddRange(d.Keys);
        }

        lTags.Sort(StrUtil.CompareNaturally);
      }
    }

    internal static void AddTags(List<string> lTags, IEnumerable<string> eNewTags)
    {
      if (lTags == null) { Debug.Assert(false); return; }
      if (eNewTags == null) { Debug.Assert(false); return; }

      lTags.AddRange(eNewTags);
      NormalizeTags(lTags);
    }

    public static string TagsToString(List<string> lTags, bool bForDisplay)
    {
      if (lTags == null) throw new ArgumentNullException("lTags");

#if DEBUG
      // The input should be normalized
      foreach (string str in lTags) { Debug.Assert(NormalizeTag(str) == str); }
      List<string> l = new List<string>(lTags);
      NormalizeTags(l);
      Debug.Assert(l.Count == lTags.Count);
#endif

      int n = lTags.Count;
      if (n == 0) return string.Empty;
      if (n == 1) return (lTags[0] ?? string.Empty);

      StringBuilder sb = new StringBuilder();
      bool bFirst = true;

      foreach (string strTag in lTags)
      {
        if (string.IsNullOrEmpty(strTag)) { Debug.Assert(false); continue; }

        if (bFirst) bFirst = false;
        else
        {
          if (bForDisplay) sb.Append(", ");
          else sb.Append(';');
        }

        sb.Append(strTag);
      }

      return sb.ToString();
    }

    public static List<string> StringToTags(string strTags)
    {
      if (strTags == null) throw new ArgumentNullException("strTags");

      List<string> lTags = new List<string>();
      if (strTags.Length == 0) return lTags;

      lTags.AddRange(strTags.Split(g_vTagSep));

      NormalizeTags(lTags);
      return lTags;
    }

    public static string Obfuscate(string strPlain)
    {
      if (strPlain == null) { Debug.Assert(false); return string.Empty; }
      if (strPlain.Length == 0) return string.Empty;

      byte[] pb = StrUtil.Utf8.GetBytes(strPlain);

      Array.Reverse(pb);
      for (int i = 0; i < pb.Length; ++i) pb[i] = (byte)(pb[i] ^ 0x65);

#if (!KeePassLibSD && !KeePassUAP)
      return Convert.ToBase64String(pb, Base64FormattingOptions.None);
#else
			return Convert.ToBase64String(pb);
#endif
    }

    public static string Deobfuscate(string strObf)
    {
      if (strObf == null) { Debug.Assert(false); return string.Empty; }
      if (strObf.Length == 0) return string.Empty;

      try
      {
        byte[] pb = Convert.FromBase64String(strObf);

        for (int i = 0; i < pb.Length; ++i) pb[i] = (byte)(pb[i] ^ 0x65);
        Array.Reverse(pb);

        return StrUtil.Utf8.GetString(pb, 0, pb.Length);
      }
      catch (Exception) { Debug.Assert(false); }

      return string.Empty;
    }

    /// <summary>
    /// Split a string and include the separators in the splitted array.
    /// </summary>
    /// <param name="str">String to split.</param>
    /// <param name="vSeps">Separators.</param>
    /// <param name="bCaseSensitive">Specifies whether separators are
    /// matched case-sensitively or not.</param>
    /// <returns>Splitted string including separators.</returns>
    public static List<string> SplitWithSep(string str, string[] vSeps,
        bool bCaseSensitive)
    {
      if (str == null) throw new ArgumentNullException("str");
      if (vSeps == null) throw new ArgumentNullException("vSeps");

      List<string> v = new List<string>();
      while (true)
      {
        int minIndex = int.MaxValue, minSep = -1;
        for (int i = 0; i < vSeps.Length; ++i)
        {
          string strSep = vSeps[i];
          if (string.IsNullOrEmpty(strSep)) { Debug.Assert(false); continue; }

          int iIndex = (bCaseSensitive ? str.IndexOf(strSep) :
              str.IndexOf(strSep, StrUtil.CaseIgnoreCmp));
          if ((iIndex >= 0) && (iIndex < minIndex))
          {
            minIndex = iIndex;
            minSep = i;
          }
        }

        if (minIndex == int.MaxValue) break;

        v.Add(str.Substring(0, minIndex));
        v.Add(vSeps[minSep]);

        str = str.Substring(minIndex + vSeps[minSep].Length);
      }

      v.Add(str);
      return v;
    }

    public static string MultiToSingleLine(string strMulti)
    {
      if (strMulti == null) { Debug.Assert(false); return string.Empty; }
      if (strMulti.Length == 0) return string.Empty;

      string str = strMulti;
      str = str.Replace("\r\n", " ");
      str = str.Replace('\r', ' ');
      str = str.Replace('\n', ' ');

      return str;
    }

    public static List<string> SplitSearchTerms(string strSearch)
    {
      List<string> l = new List<string>();
      if (strSearch == null) { Debug.Assert(false); return l; }

      StringBuilder sbTerm = new StringBuilder();
      bool bQuoted = false;

      for (int i = 0; i < strSearch.Length; ++i)
      {
        char ch = strSearch[i];

        if (((ch == ' ') || (ch == '\t') || (ch == '\r') ||
            (ch == '\n')) && !bQuoted)
        {
          if (sbTerm.Length != 0)
          {
            l.Add(sbTerm.ToString());
            sbTerm.Remove(0, sbTerm.Length);
          }
        }
        else if (ch == '\"') bQuoted = !bQuoted;
        else sbTerm.Append(ch);
      }
      if (sbTerm.Length != 0) l.Add(sbTerm.ToString());

      return l;
    }

    public static int CompareLengthGt(string x, string y)
    {
      if (x.Length == y.Length) return 0;
      return ((x.Length > y.Length) ? -1 : 1);
    }

    public static bool IsDataUri(string strUri)
    {
      return IsDataUri(strUri, null);
    }

    public static bool IsDataUri(string strUri, string strReqMediaType)
    {
      if (strUri == null) { Debug.Assert(false); return false; }
      // strReqMediaType may be null

      const string strPrefix = "data:";
      if (!strUri.StartsWith(strPrefix, StrUtil.CaseIgnoreCmp))
        return false;

      int iC = strUri.IndexOf(',');
      if (iC < 0) return false;

      if (!string.IsNullOrEmpty(strReqMediaType))
      {
        int iS = strUri.IndexOf(';', 0, iC);
        int iTerm = ((iS >= 0) ? iS : iC);

        string strMedia = strUri.Substring(strPrefix.Length,
            iTerm - strPrefix.Length);
        if (!strMedia.Equals(strReqMediaType, StrUtil.CaseIgnoreCmp))
          return false;
      }

      return true;
    }

    /// <summary>
    /// Create a data URI (according to RFC 2397).
    /// </summary>
    /// <param name="pbData">Data to encode.</param>
    /// <param name="strMediaType">Optional MIME type. If <c>null</c>,
    /// an appropriate type is used.</param>
    /// <returns>Data URI.</returns>
    public static string DataToDataUri(byte[] pbData, string strMediaType)
    {
      if (pbData == null) throw new ArgumentNullException("pbData");

      if (strMediaType == null) strMediaType = "application/octet-stream";

#if (!KeePassLibSD && !KeePassUAP)
      return ("data:" + strMediaType + ";base64," + Convert.ToBase64String(
          pbData, Base64FormattingOptions.None));
#else
			return ("data:" + strMediaType + ";base64," + Convert.ToBase64String(
				pbData));
#endif
    }

    /// <summary>
    /// Convert a data URI (according to RFC 2397) to binary data.
    /// </summary>
    /// <param name="strDataUri">Data URI to decode.</param>
    /// <returns>Decoded binary data.</returns>
    public static byte[] DataUriToData(string strDataUri)
    {
      if (strDataUri == null) throw new ArgumentNullException("strDataUri");
      if (!strDataUri.StartsWith("data:", StrUtil.CaseIgnoreCmp)) return null;

      int iSep = strDataUri.IndexOf(',');
      if (iSep < 0) return null;

      string strDesc = strDataUri.Substring(5, iSep - 5);
      bool bBase64 = strDesc.EndsWith(";base64", StrUtil.CaseIgnoreCmp);

      string strData = strDataUri.Substring(iSep + 1);

      if (bBase64) return Convert.FromBase64String(strData);

      MemoryStream ms = new MemoryStream();
      Encoding enc = Encoding.ASCII;

      string[] v = strData.Split('%');
      byte[] pb = enc.GetBytes(v[0]);
      ms.Write(pb, 0, pb.Length);
      for (int i = 1; i < v.Length; ++i)
      {
        ms.WriteByte(Convert.ToByte(v[i].Substring(0, 2), 16));
        pb = enc.GetBytes(v[i].Substring(2));
        ms.Write(pb, 0, pb.Length);
      }

      pb = ms.ToArray();
      ms.Close();
      return pb;
    }

    // https://www.iana.org/assignments/media-types/media-types.xhtml
    private static readonly string[] g_vMediaTypePfx = new string[] {
            "application/", "audio/", "example/", "font/", "image/",
            "message/", "model/", "multipart/", "text/", "video/"
        };
    internal static bool IsMediaType(string str)
    {
      if (str == null) { Debug.Assert(false); return false; }
      if (str.Length == 0) return false;

      foreach (string strPfx in g_vMediaTypePfx)
      {
        if (str.StartsWith(strPfx, StrUtil.CaseIgnoreCmp))
          return true;
      }

      return false;
    }

    internal static string GetCustomMediaType(string strFormat)
    {
      if (strFormat == null)
      {
        Debug.Assert(false);
        return "application/octet-stream";
      }

      if (IsMediaType(strFormat)) return strFormat;

      StringBuilder sb = new StringBuilder();
      for (int i = 0; i < strFormat.Length; ++i)
      {
        char ch = strFormat[i];

        if (((ch >= 'A') && (ch <= 'Z')) || ((ch >= 'a') && (ch <= 'z')) ||
            ((ch >= '0') && (ch <= '9')))
          sb.Append(ch);
        else if ((sb.Length != 0) && ((ch == '-') || (ch == '_')))
          sb.Append(ch);
        else { Debug.Assert(false); }
      }

      if (sb.Length == 0) return "application/octet-stream";

      return ("application/vnd." + PwDefs.ShortProductName +
          "." + sb.ToString());
    }

    /// <summary>
    /// Remove placeholders from a string (wrapped in '{' and '}').
    /// This doesn't remove environment variables (wrapped in '%').
    /// </summary>
    public static string RemovePlaceholders(string str)
    {
      if (str == null) { Debug.Assert(false); return string.Empty; }

      while (true)
      {
        int iPlhStart = str.IndexOf('{');
        if (iPlhStart < 0) break;

        int iPlhEnd = str.IndexOf('}', iPlhStart); // '{' might be at end
        if (iPlhEnd < 0) break;

        str = (str.Substring(0, iPlhStart) + str.Substring(iPlhEnd + 1));
      }

      return str;
    }

    public static StrEncodingInfo GetEncoding(StrEncodingType t)
    {
      foreach (StrEncodingInfo sei in StrUtil.Encodings)
      {
        if (sei.Type == t) return sei;
      }

      return null;
    }

    public static StrEncodingInfo GetEncoding(string strName)
    {
      foreach (StrEncodingInfo sei in StrUtil.Encodings)
      {
        if (sei.Name == strName) return sei;
      }

      return null;
    }

    private static string[] m_vPrefSepChars = null;
    /// <summary>
    /// Find a character that does not occur within a given text.
    /// </summary>
    public static char GetUnusedChar(string strText)
    {
      if (strText == null) { Debug.Assert(false); return '@'; }

      if (m_vPrefSepChars == null)
        m_vPrefSepChars = new string[5] {
                    "@!$%#/\\:;,.*-_?",
                    PwCharSet.UpperCase, PwCharSet.LowerCase,
                    PwCharSet.Digits, PwCharSet.PrintableAsciiSpecial
                };

      for (int i = 0; i < m_vPrefSepChars.Length; ++i)
      {
        foreach (char ch in m_vPrefSepChars[i])
        {
          if (strText.IndexOf(ch) < 0) return ch;
        }
      }

      for (char ch = '\u00C0'; ch < char.MaxValue; ++ch)
      {
        if (strText.IndexOf(ch) < 0) return ch;
      }

      return char.MinValue;
    }

    public static char ByteToSafeChar(byte bt)
    {
      const char chDefault = '.';

      // 00-1F are C0 control chars
      if (bt < 0x20) return chDefault;

      // 20-7F are basic Latin; 7F is DEL
      if (bt < 0x7F) return (char)bt;

      // 80-9F are C1 control chars
      if (bt < 0xA0) return chDefault;

      // A0-FF are Latin-1 supplement; AD is soft hyphen
      if (bt == 0xAD) return '-';
      return (char)bt;
    }

    public static int Count(string str, string strNeedle)
    {
      if (str == null) { Debug.Assert(false); return 0; }
      if (string.IsNullOrEmpty(strNeedle)) { Debug.Assert(false); return 0; }

      int iOffset = 0, iCount = 0;
      while (iOffset < str.Length)
      {
        int p = str.IndexOf(strNeedle, iOffset);
        if (p < 0) break;

        ++iCount;
        iOffset = p + 1;
      }

      return iCount;
    }

    internal static string ReplaceNulls(string str)
    {
      if (str == null) { Debug.Assert(false); return null; }

      if (str.IndexOf('\0') < 0) return str;

      // Replacing null characters by spaces is the
      // behavior of Notepad (on Windows 10)
      return str.Replace('\0', ' ');
    }

    // https://sourceforge.net/p/keepass/discussion/329220/thread/f98dece5/
    internal static string EnsureLtrPath(string strPath)
    {
      if (strPath == null) { Debug.Assert(false); return string.Empty; }

      string str = strPath;

      // U+200E = left-to-right mark
      str = str.Replace("\\", "\\\u200E");
      str = str.Replace("/", "/\u200E");
      str = str.Replace("\u200E\u200E", "\u200E"); // Remove duplicates

      return str;
    }

    internal static bool IsValid(string str)
    {
      if (str == null) { Debug.Assert(false); return false; }

      int cc = str.Length;
      for (int i = 0; i < cc; ++i)
      {
        char ch = str[i];
        if (ch == '\0') return false;

        if (char.IsLowSurrogate(ch)) return false;
        if (char.IsHighSurrogate(ch))
        {
          if (++i >= cc) return false; // High surrogate at end
          if (!char.IsLowSurrogate(str[i])) return false;

          UnicodeCategory uc2 = char.GetUnicodeCategory(str, i - 1);
          if (uc2 == UnicodeCategory.OtherNotAssigned) return false;

          continue;
        }

        UnicodeCategory uc = char.GetUnicodeCategory(ch);
        if (uc == UnicodeCategory.OtherNotAssigned) return false;
      }

      return true;
    }

    internal static string RemoveWhiteSpace(string str)
    {
      if (str == null) { Debug.Assert(false); return string.Empty; }

      int cc = str.Length;
      if (cc == 0) return string.Empty;

      StringBuilder sb = new StringBuilder();

      for (int i = 0; i < cc; ++i)
      {
        char ch = str[i];
        if (char.IsWhiteSpace(ch)) continue;
        sb.Append(ch);
      }

      return sb.ToString();
    }
  }
}
